package com.bardsoftware.foronuvolo.server;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.appengine.repackaged.com.google.common.collect.Lists;

public class WikiMessageFormatter {
    private static final Logger LOGGER = Logger.getLogger(WikiMessageFormatter.class.getName());
    static final int LONG_TEXT_THRESHOLD = 300;

    private static class FormatSymbol {
        static FormatSymbol BOLD_CLOSING = new FormatSymbol("*", "</b>", null);
        static FormatSymbol ITALIC_CLOSING = new FormatSymbol("_", "</i>", null);
        static FormatSymbol CODE_CLOSING = new FormatSymbol("`", "</code>", null);
        static FormatSymbol HEADING_CLOSING = new FormatSymbol("=", "</h1>", null);
        static FormatSymbol BOLD = new FormatSymbol("*", "<b>", BOLD_CLOSING);
        static FormatSymbol ITALIC = new FormatSymbol("_", "<i>", ITALIC_CLOSING);
        static FormatSymbol CODE = new FormatSymbol("`", "<code>", CODE_CLOSING);
        static FormatSymbol HEADING = new FormatSymbol("=", "<h1>", HEADING_CLOSING);

        private final String myHtmlTag;
        final String symbol;
        final FormatSymbol closing;
        boolean isValid = true;

        FormatSymbol(String symbol, String htmlTag, FormatSymbol closing) {
            this.symbol = symbol;
            myHtmlTag = htmlTag;
            this.closing = closing;
        }
        public FormatSymbol getClosingSymbol() {
            return this.closing;
        }
        public FormatSymbol getPlaceholder() {
            return new FormatSymbol(this.symbol, this.myHtmlTag, this.closing);
        }
        public static FormatSymbol[] opening() {
            return new FormatSymbol[] {BOLD, ITALIC, CODE, HEADING};
        }
        public static FormatSymbol[] closing() {
            return new FormatSymbol[] {BOLD_CLOSING, ITALIC_CLOSING, CODE_CLOSING, HEADING_CLOSING};
        }
        public void setInvalid() {
            this.isValid = false;
        }
        public String toString() {
            return isValid ? myHtmlTag : symbol;
        }
        static FormatSymbol findClosing(String symbol) {
            if (BOLD_CLOSING.symbol.equals(symbol)) {
                return BOLD_CLOSING;
            }
            if (ITALIC_CLOSING.symbol.equals(symbol)) {
                return ITALIC_CLOSING;
            }
            if (CODE_CLOSING.symbol.equals(symbol)) {
                return CODE_CLOSING;
            }
            if (HEADING_CLOSING.symbol.equals(symbol)) {
                return HEADING_CLOSING;
            }
            return null;
        }
    }

    private static class Checkpoint {
        private final String myResult;
        private final boolean isEndOfSentence;

        public Checkpoint(String result, boolean endOfSentence) {
            myResult = result;
            isEndOfSentence = endOfSentence;
        }
        public int getLength() {
            return myResult.length();
        }
        public String getResult() {
            return myResult;
        }
    }
    private static class InternalState {
        private static final int CHUNK_LENGTH = 10;
        Stack<FormatSymbol> formatStack = new Stack<FormatSymbol>();
        StringBuilder result = new StringBuilder();
        List<FormatSymbol> formatList = new ArrayList<FormatSymbol>();
        private int myChunkCount = 0;
        private int mySentenceCount = 0;
        private List<Checkpoint> myCheckpoints = new ArrayList<Checkpoint>();
        private boolean isCheckpointing = false;
        private boolean isInsideHtml;

        public void incrementParagraph() {
            incrementSentence();
        }
        public void incrementWord() {
            if (!isCheckpointing) {
                return;
            }
            int chunkCount = result.length() / CHUNK_LENGTH;
            if (chunkCount > myChunkCount && !hasPendingFormatting()) {
                addCheckpoint(false);
            }
            myChunkCount = chunkCount;
        }
        private boolean hasPendingFormatting() {
            return !formatStack.isEmpty();
        }
        public void incrementSentence() {
            if (!isCheckpointing) {
                return;
            }
            ++mySentenceCount;
            if (!hasPendingFormatting()) {
                addCheckpoint(true);
            }
        }
        private void addCheckpoint(boolean endOfSentence) {
            String checkpointValue = format();
            if (checkpointValue != null) {
                myCheckpoints.add(new Checkpoint(checkpointValue,  endOfSentence));
            }
        }
        public String format() {
            try {
                return MessageFormat.format(result.toString(), formatList.toArray());
            }
            catch (Throwable e) {
                LOGGER.log(Level.SEVERE, "Failed to format:\n" + result.toString() + "\n\n with format list:\n" + formatList, e);
                return null;
            }
        }
        public void setRememberCheckpoints(boolean b) {
            isCheckpointing = b;
        }
        public List<Checkpoint> getCheckpoints() {
            return myCheckpoints;
        }
        public void eof() {
            if (isCheckpointing) {
                ++mySentenceCount;
                addCheckpoint(true);
            }
        }
        public void setInsideHtml(boolean b) {
            isInsideHtml = b;
        }
        public boolean isInsideHtml() {
            return isInsideHtml;
        }
    }

    private static abstract class State {
        public abstract State consume(String token);

        protected final InternalState internalState;
        protected State(InternalState internalState) {
            this.internalState = internalState;
        }

        protected static boolean isWhitespace(String token) {
            return token.length() == 1 && Character.isWhitespace(token.charAt(0));
        }

        public void consumeEOF() {
            internalState.eof();
        }
    }

    private static class PlainTextState extends State {
        public PlainTextState(InternalState internalState) {
            super(internalState);
        }

        public State consume(String token) {
            if (token.length() == 1) {
                if (Character.isWhitespace(token.charAt(0))) {
                    return new WhitespaceState(internalState).consume(token);
                }
                if ("<".equals(token)) {
                    internalState.setInsideHtml(true);
                }
                else if (">".equals(token)) {
                    internalState.setInsideHtml(false);
                }
                if ("{".equals(token)) {
                    token = "&#123;";
                }
                else if ("}".equals(token)) {
                    token = "&#125;";
                }
                if (!internalState.isInsideHtml()) {
                    if ("'".equals(token)) {
                        token = "''";
                    }
                    else {
                        FormatSymbol closing = FormatSymbol.findClosing(token);
                        if (closing != null && !internalState.formatStack.isEmpty()) {
                            return new CloseFormattingState(internalState).consume(token);
                        }
                    }
                }
            } else if ("http".equals(token) || "https".equals(token)) {
                return new UrlAccumulatorState(internalState).consume(token);
            }
            internalState.result.append(token);
            if (".".equals(token)) {
                internalState.incrementSentence();
            }
            return this;
        }
    }

    private static class UrlAccumulatorState extends State {
        private StringBuilder myUrlBuilder = new StringBuilder();
        protected UrlAccumulatorState(InternalState internalState) {
            super(internalState);
        }

        @Override
        public State consume(String token) {
            if (myUrlBuilder.length() == 0) {
                assert "http".equals(token);
                myUrlBuilder.append(token);
                return this;
            }
            if (isWhitespace(token)) {
                processAccumulatedUrl();
                return new WhitespaceState(internalState).consume(token);
            }
            myUrlBuilder.append(token);
            return this;
        }

        private void processAccumulatedUrl() {
            String builtUrl = myUrlBuilder.toString();
            internalState.result.append("<a href=\"" + builtUrl + "\">" + builtUrl + "</a>");
        }

        @Override
        public void consumeEOF() {
            processAccumulatedUrl();
            super.consumeEOF();
        }
    }
    private static class WhitespaceState extends State {
        protected WhitespaceState(InternalState internalState) {
            super(internalState);
            internalState.incrementWord();
        }
        @Override
        public State consume(String token) {
            if (token.length()==1 && Character.isWhitespace(token.charAt(0))) {
                if ("\n".equals(token)) {
                    internalState.result.append("<br>");
                    internalState.incrementParagraph();
                } else {
                    internalState.result.append(token);
                }
                return this;
            }
            FormatSymbol closing = FormatSymbol.findClosing(token);
            if (closing != null) {
                if (!internalState.formatStack.isEmpty()
                    && internalState.formatStack.peek().getClosingSymbol() == closing) {
                    return new CloseFormattingState(internalState).consume(token);
                }
                else {
                    return new OpenFormattingState(internalState).consume(token);
                }
            }
            return new PlainTextState(internalState).consume(token);
        }

    }
    private static class OpenFormattingState extends State {
        public OpenFormattingState(InternalState internalState) {
            super(internalState);
        }

        public State consume(String token) {
            for (FormatSymbol fs : FormatSymbol.opening()) {
                if (token.equals(fs.symbol)) {
                    internalState.result.append("{" + internalState.formatList.size() +"}");
                    FormatSymbol placeholder = fs.getPlaceholder();
                    internalState.formatList.add(placeholder);
                    internalState.formatStack.push(placeholder);
                    return new OpenFormattingState(internalState);
                }
            }
            assert !internalState.formatStack.isEmpty();
            return new PlainTextState(internalState).consume(token);
        }
    }

    private static class CloseFormattingState extends State {
        public CloseFormattingState(InternalState internalState) {
            super(internalState);
        }

        public State consume(String token) {
            FormatSymbol closing = FormatSymbol.findClosing(token);
            FormatSymbol opening = internalState.formatStack.pop();
            if (opening.getClosingSymbol() == closing) {
                internalState.result.append("{" + internalState.formatList.size() +"}");
                internalState.formatList.add(closing);
            } else {
                opening.setInvalid();
                internalState.result.append(closing.symbol);
            }
            return new PlainTextState(internalState);
        }
    }

    public static class Result {
        public final String text;
        public final boolean isTruncated;
        Result(String text, boolean isTruncated) {
            this.text = text;
            this.isTruncated = isTruncated;
        }
    }

    private int myLongTextThreshold;

    public WikiMessageFormatter() {
        this (Integer.MAX_VALUE);
    }

    public WikiMessageFormatter(int longTextThreshold) {
        myLongTextThreshold = longTextThreshold;
    }

    public Result format(String text) {
        InternalState internalState = new InternalState();
        boolean longText = text.length() > myLongTextThreshold;
        if (longText) {
            internalState.setRememberCheckpoints(true);
        }
        State state = new WhitespaceState(internalState);
        for (StringTokenizer tkzr = new StringTokenizer(text, " *_=.,;:-(){}[]<>?!'`\t\n\r\f", true); tkzr.hasMoreTokens();) {
            String token = tkzr.nextToken();
            state = state.consume(token);
        }
        state.consumeEOF();
        for (FormatSymbol fs : internalState.formatStack) {
            fs.setInvalid();
        }
        if (longText) {
            List<Checkpoint> allCheckPoints = internalState.getCheckpoints();
            Checkpoint checkpoint = cutTail(Lists.newArrayList(allCheckPoints), text);
            if (checkpoint != allCheckPoints.get(allCheckPoints.size()-1)) {
                return new Result(checkpoint.getResult(), true);
            }
        }
        return new Result(internalState.format(), false);
    }


    private Checkpoint cutTail(List<Checkpoint> checkpoints, String text) {
        assert !checkpoints.isEmpty() : "at least one checkpoint must be created by EOF. text:\n" + text;
        Collections.sort(checkpoints, new Comparator<Checkpoint>() {
            public int compare(Checkpoint o1, Checkpoint o2) {
                if (o1 == null || o2 == null) {
                    return 0;
                }
                int o1diff = Math.abs(myLongTextThreshold - o1.getLength());
                int o2diff = Math.abs(myLongTextThreshold - o2.getLength());
                if (o1diff != o2diff) {
                    return o1diff - o2diff;
                }
                return o1.getLength() - o2.getLength();
            }
        });
        Checkpoint lastGood = null;
        for (Checkpoint c : checkpoints) {
            if (c.isEndOfSentence) {
                lastGood = c;
                break;
            }
            if (Math.abs(myLongTextThreshold - c.getLength()) + 0.1f / myLongTextThreshold > 0.25) {
                break;
            }
        }
        if (lastGood != null) {
            return lastGood;
        }
        return checkpoints.get(0);
    }

}
